[Previous] [Next]

TextBox Controls

TextBox controls offer a natural way for users to enter a value in your program. For this reason, they tend to be the most frequently used controls in the majority of Windows applications. TextBox controls, which have a great many properties and events, are also among the most complex intrinsic controls. In this section, I guide you through the most useful properties of TextBox controls and show how to solve some of the problems that you're likely to encounter.

After you place a TextBox control on a form, you must set a few basic properties. The first thing I do as soon as I create a new TextBox control is clear its Textproperty. If this is a multiline field, I also set the MultiLineproperty to True.

You can set the Alignment property of TextBox controls to left align, right align, or center the contents of the control. Right-aligned TextBox controls are especially useful when you're displaying numeric values. But you should be aware of the following quirk: while this property always works correctly when the Multiline property is set to True, it works with single-line controls only under Microsoft Windows 98, Microsoft Windows NT 4 with Service Pack 3, or later versions. Under previous versions of Windows 9x or Windows NT, no error is raised but single-line TextBox controls ignore the Alignment property and always align their contents to the left.

You can prevent the user from changing the contents of a TextBox control by setting its Lockedproperty to True. You usually do this if the control contains the result of a calculation or displays a field taken from a database opened in read-only mode. In most cases, you can achieve the same result using a Label control with a border and white background, but a locked TextBox control also permits your users to copy the value to the Clipboard and scroll through it if it's too large for the field's width.

If you're dealing with a numeric field, you probably want to set a limit on the number of characters that the user can enter in the field. You can do it very easily using the MaxLength property. A 0 value (the default) means that you can enter any number of characters; any positive value N enforces a limit to the length of the field's contents to be N characters long.

If you're creating password fields, you should set the PasswordChar property to a character string, typically an asterisk. In this case, your program can read and modify the contents of this TextBox control as usual, but users see only a row of asterisks.

CAUTION
Password-protected TextBox controls effectively disable the Ctrl+X and Ctrl+C keyboard shortcuts, so malicious users can't steal a password entered by another user. If, however, your application includes an Edit menu with all the usual clipboard commands, it's up to you to disable the Copy and Cut commands when the focus is on a password-protected field.

You can set other properties for a better appearance of the control—the Font property, for example. In addition, you can set the ToolTipText property to help users understand what the TextBox control is for. You can also make borderless TextBox controls by setting their BorderStyle property to 0-None, but controls like these don't appear frequently in Windows applications. In general, you can't do much else with a TextBox control at design time. The most interesting things can be done only through code.

Run-Time Properties

The Text property is the one you'll reference most often in code, and conveniently it's the default property for the TextBox control. Three other frequently used properties are these:

TIP
When you want to append text to a TextBox control, you should use the following code (instead of using the concatenation operator) to reduce flickering and improve performance:

Text1.SelStart = Len(Text1.Text)
Text1.SelText = StringToBeAdded

One of the typical operations you could find yourself performing with these properties is selecting the entire contents of a TextBox control. You often do it when the caret enters the field so that the user can quickly override the existing value with a new one, or start editing it by pressing any arrow key:

Private Sub Text1_GotFocus()
    Text1.SelStart = 0 
    ' A very high value always does the trick.
    Text1.SelLength = 9999
End Sub

Always set the SelStart property first and then the SelLength or SelText properties. When you assign a new value to the SelStart property, the other two are automatically reset to 0 and an empty string respectively, thus overriding your previous settings.

Trapping Keyboard Activity

TextBox controls support KeyDown, KeyPress, and KeyUp standard events, which Chapter 2 covered. One thing that you will often do is prevent the user from entering invalid keys. A typical example of where this safeguard is needed is a numeric field, for which you need to filter out all nondigit keys:

Private Sub Text1_KeyPress(KeyAscii As Integer)
    Select Case KeyAscii
        Case Is < 32               ' Control keys are OK.
        Case 48 To 57              ' This is a digit.
        Case Else                  ' Reject any other key.
            KeyAscii = 0
    End Select
End Sub

You should never reject keys whose ANSI code is less than 32, a group that includes important keys such as Backspace, Escape, Tab, and Enter. Also note that a few control keys will make your TextBox beep if it doesn't know what to do with them—for example, a single-line TextBox control doesn't know what to do with an Enter key.

CAUTION
Don't assume that the KeyPress event will trap all control keys under all conditions. For example, the KeyPress event can process the Enter key only if there's no CommandButton control on the form whose Default property is set to True. If the form has a default push button, the effect of pressing the Enter key is clicking on that button. Similarly, no Escape key goes through this event if there's a Cancel button on the form. Finally, the Tab control key is trapped by a KeyPress event only if there isn't any other control on the form whose TabStop property is True.

You can use the KeyDown event procedure to allow users to increase and decrease the current value using Up and Down arrow keys, as you see here:

Private Sub Text1_KeyDown(KeyCode As Integer, Shift As Integer)
    Select Case KeyCode
        Case vbKeyUp
            Text1.Text = CDbl(Text1.Text) + 1
        Case vbKeyDown
            Text1.Text = CDbl(Text1.Text)  -1
    End Select
End Sub

NOTE
There's a bug in the implementation of TextBox ready-only controls. When the Locked property is set to True, the Ctrl+C key combination doesn't correctly copy the selected text to the Clipboard, and you must manually implement this capability by writing code in the KeyPress event procedure.

Validation Routines for Numbers

Although trapping invalid keys in the KeyPress or KeyDown event procedures seems a great idea at first, when you throw your application to inexperienced users you soon realize that there are many ways for them to enter invalid data. Depending on what you do with this data, your application can come to an abrupt end with a run-time error or—much worse—it can appear to work correctly while it delivers bogus results. What you really need is a bullet-proof method to trap invalid values.

Before I offer you a decent solution to the problem, let me explain why you can't rely solely on trapping invalid keys for your validation chores. What if the user pastes an invalid value from the clipboard? Well, you might say, let's trap the Ctrl+V and Shift+Ins key combinations to prevent the user from doing that! Unfortunately, Visual Basic's TextBox controls offer a default edit menu that lets users perform any clipboard operation by simply right-clicking on them. Fortunately, there's a way around this problem: Instead of trapping a key before it gets to the TextBox control, you trap its effect in the Change event and reject it if it doesn't pass your test. But this makes the structure of the code a little more complex than you might anticipate:

' Form-level variables
Dim saveText As String
Dim saveSelStart As Long

Private Sub Text1_GotFocus()
    ' Save values when the control gets the focus.
    saveText = Text1.Text
    saveSelStart = Text1.SelStart
End Sub

Private Sub Text1_Change()
    ' Avoid nested calls.
    Static nestedCall As Boolean
    If nestedCall Then Exit Sub

    ' Test the control's value here.
    If IsNumeric(Text1.Text) Then
        ' If value is OK, save values.
        saveText = Text1.Text
        saveSelStart = Text1.SelStart
    Else
        ' Prepare to handle a nested call. 
        nestedCall = True
        Text1.Text = saveText
        nestedCall = False
        Text1.SelStart = saveSelStart
    End If
End Sub

Private Sub Text1_KeyUp(KeyCode As Integer, Shift As Integer)
    saveSelStart = Text1.SelStart
End Sub
Private Sub Text1_MouseDown(Button As Integer, _
    Shift As Integer, X As Single, Y As Single)
    saveSelStart = Text1.SelStart
End Sub
Private Sub Text1_MouseMove(Button As Integer, _
    Shift As Integer, X As Single, Y As Single)
    saveSelStart = Text1.SelStart
End Sub

If the control's value doesn't pass your tests in the Change event procedure, you must restore its previous valid value; this action recursively fires a Change event, and you must prepare yourself to neutralize this nested call. You might wonder why you also need to trap the KeyUp, MouseDown, and MouseMove events: The reason is that you always need to keep track of the last valid position for the insertion point because the end user could move it using arrow keys or the mouse.

The preceding code snippet uses the IsNumeric function to trap invalid data. You should be aware that this function isn't robust enough for most real-world applications. For example, the IsNumeric function incorrectly considers these strings as valid numbers:

123,,,123
345-
$1234     ' What if it isn't a currency field?
2.4E10    ' What if I don't want to support scientific notation?

To cope with this issue, I have prepared an alternative function, which you can modify for your particular purposes. (For instance, you can add support for a currency symbol or the comma as the decimal separator.) Note that this function always returns True when it's passed a null string, so you might need to perform additional tests if the user isn't allowed to leave the field blank:

Function CheckNumeric(text As String, DecValue As Boolean) As Boolean
    Dim i As Integer
    For i = 1 To Len(text)
        Select Case Mid$(text, i, 1)
            Case "0" To "9"
            Case "-", "+"
                ' Minus/plus signs are only allowed as leading chars.
                If i > 1 Then Exit Function
            Case "."
                ' Exit if decimal values not allowed.
                If Not DecValue Then Exit Function
                ' Only one decimal separator is allowed.
                If InStr(text, ".") < i Then Exit Function
            Case Else
                ' Reject all other characters.
                Exit Function
        End Select
    Next
    CheckNumeric = True
End Function

If your TextBox controls are expected to contain other types of data, you might be tempted to reuse the same validation framework I showed you previously—including all the code in the GotFocus, Change, KeyUp, MouseDown, and MouseMove event procedures—and replace only the call to IsNumeric with a call to your custom validation routine. Things aren't as simple as they appear at first, however. Say that you have a date field: Can you use the IsDate function to validate it from within the Change event? The answer is, of course, no. In fact, as you enter the first digit of your date value, IsDate returns False and the routine therefore prevents you from entering the remaining characters, and so preventing you from entering any value.

This example explains why a key-level validation isn't always the best answer to your validation needs. For this reason, most Visual Basic programmers prefer to rely on field-level validation and test the values only when the user moves the input focus to another field in the form. I explain field-level validation in the next section.

The CausesValidation Property and the Validate Event

Visual Basic 6 has finally come up with a solution for most of the validation issues that have afflicted Visual Basic developers for years. As you'll see in a moment, the Visual Basic 6 approach is simple and clean; it really astonishes me that it took six language versions to deliver such a lifesaver. The keys to the new validation features are the Validate event and the CausesValidation property. They work together as follows: When the input focus leaves a control, Visual Basic checks the CausesValidation property of the control that is about to receive the focus. If this property is True, Visual Basic fires the Validate event in the control that's about to lose the focus, thus giving the programmer a chance to validate its contents and, if necessary, cancel the focus shift.

Let's try a practical example. Imagine that you have five controls on a form: a required field (a TextBox control, txtRequired, that can't contain an empty string), a numeric field, txtNumeric, that expects a value in the range 1 through 1000, and three push buttons: OK, Cancel, and Help. (See Figure 3-1.) You don't want to perform validation if the user presses the Cancel or Help buttons, so you set their CausesValidation properties to False. The default value for this property is True, so you don't have to modify it for the other controls. Run the sample program on the companion CD, type something in the required TextBox, and then move to the second field. Because the second field's CausesValidation property is True, Visual Basic fires a Validate event in the first TextBox control:

Private Sub txtRequired_Validate(Cancel As Boolean)
    ' Check that field is not empty.
    If txtRequired.Text = "" Then
        MsgBox "Please enter something here", vbExclamation
        Cancel = True
    End If
End Sub

If the Cancel parameter is set to True, Visual Basic cancels the user's action and takes the input focus back on the txtRequired control: No other GotFocus and LostFocus events are generated. On the other hand, if you typed something in the required field, the focus will now be on the second field (the numeric text box). Try clicking on the Help or Cancel buttons: No Validate event will fire this time because you set the CausesValidation property for each of these controls to False. Instead, click on the OK button to execute the Validate event of the numeric field, where you can check it for invalid characters and valid range.

Click to view at full size.

Figure 3-1. A demonstration program that lets you experiment with the new Visual Basic Validate features.

Private Sub txtNumeric_Validate(Cancel As Boolean)
    If Not IsNumeric(txtNumeric.Text) Then
        Cancel = True
    ElseIf CDbl(txtNumeric.Text) < 1 Or CDbl(txtNumeric.Text) > 1000 Then
        Cancel = True
    End If
    If Cancel Then
        MsgBox "Please enter a number in range [1-1000]", vbExclamation
    End If
End Sub

In some circumstances, you might want to programmatically validate the control that has the focus without waiting for the user to move the input focus. You can do it with the form's ValidateControls method, which forces the Validate event of the control that has the input focus. Typically, you do it when the user closes the form:

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
    ' You can't close this form without validating the current field.
    If UnloadMode = vbFormControlMenu Then
        On Error Resume Next
        ValidateControls
        If Err = 380 Then
            ' The current field failed validation.
            Cancel = True
        End If
    End If
End Sub

Checking the UnloadMode parameter is important; otherwise, your application will mistakenly execute a ValidateControls method when the user clicks on the Cancel button. Note that ValidateControls returns an error 380 if Cancel was set in the Validate event procedure of the control that had the focus.

CAUTION
Visual Basic 6's validation scheme has two flaws, though. If your form has a CommandButton whose Default property is set to True, pressing the Enter key while the input focus is on another control results in a click on the CommandButton control but doesn't fire a Validate event, even if the CausesValidation property of the CommandButton control is set to True. The only way to solve this problem is to invoke the ValidateControls method from within the default CommandButton control's Click event procedure.

The second flaw is that the Validate event doesn't fire when you're moving the focus from a control whose CausesValidation property is False, even if the control that receives the focus has its CausesValidation property set to True.

The new Visual Basic 6 validation mechanism is simple and can be implemented with little effort. But it isn't the magic answer to all your validation needs. In fact, this technique can only enforce field-level validation; it does nothing for record-level validation. In other words, it ensures that one particular field is correct, not that all fields in the form contain valid data. To see what I mean, run the demonstration program, enter a string in the first field, and press Alt+F4 to close the form. Your code won't raise an error, even if the second field doesn't contain a valid number! Fortunately, it doesn't take much to create a generic routine that forces each control on the form to validate itself:

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
    ' You can't close this form without validating all the fields on it.
    If UnloadMode = vbFormControlMenu Then
        On Error Resume Next
        Dim ctrl As Control
        ' Give the focus to each control on the form, and then
        ' validate it.
        For Each ctrl In Controls
            Err.Clear
            ctrl.SetFocus
            If Err = 0 Then
                ' Don't validate controls that can't receive input focus.
                ValidateControls
                If Err = 380 Then
                    ' Validation failed, refuse to close.
                    Cancel = True: Exit Sub
                End If
            End If
        Next
    End If
End Sub

The CausesValidation property and the Validate event are shared by all the intrinsic controls that are able to get the focus as well as by most external ActiveX controls, even those not specifically written for Visual Basic. This is possible because they are extender features, provided by the Visual Basic runtime to all the controls placed on a form's surface.

TIP
One Visual Basic operator has great potential when it comes time to validate complex strings but is neglected by most Visual Basic developers. Let's say you have a product code that consists of two uppercase characters followed by exactly three digits. You might think that you need some complex string functions to validate such a string until you try the Like operator, as follows:

If "AX123" Like "[A-Z][A-Z]###" Then Print "OK"

See Chapter 5 for more information about the Like operator.

Auto-Tabbing Fields

Users aren't usually delighted to spend all their time at the keyboard. Your job as a programmer is to make their jobs easier, and so you should strive to streamline their everyday work as much as possible. One way to apply this concept is to provide them with auto-tabbing fields, which are fields that automatically advance users to the next field in the Tab order as soon as they enter a valid value. Most often, auto-tabbing fields are those TextBox controls whose MaxLength property has been assigned a non-null value. Implementing such an auto-tabbing field in Visual Basic is straightforward:

Private Sub Text1_Change()
    If Len(Text1.Text) = Text1.MaxLength Then SendKeys "{Tab}"
End Sub

The trick, as you see, is to have your program provide the Tab key on behalf of your user. In some cases, this simple approach doesn't work—for example, when you paste a long string into the field. You might want to write code that works around this and other shortcomings. Auto-tabbing is a nice feature but not vital to the application, so whether you write a workaround or not isn't a real problem in most cases.

Formatting Text

Many business applications let you enter data in one format and then display it in another. For example, numeric values can be formatted with thousand separators and a fixed number of decimal digits. Currency values might have a $ symbol (or whatever your national currency symbol is) automatically inserted. Phone numbers can be formatted with dashes to split into groups of digits. Credit-card numbers can be made more readable with embedded spaces. Dates can be shown in long-date format ("September 10, 1999"). And so on.

The LostFocus event is an ideal occasion to format the contents of a TextBox control as soon as the input focus leaves it. In most cases, you can perform all your formatting chores using the Format function. For example, you can add thousand separators to a numeric value in the txtNumber control using this code:

Private Sub txtNumber_LostFocus()
    On Error Resume Next
    txtNumber.Text = Format(CDbl(txtNumber.Text), _
        "#,###,###,##0.######")
End Sub

When the field regains the focus, you'll want to get rid of those thousand separators. You can do it easily using the CDbl function:

Private Sub txtNumber_GotFocus()
    ' On Error is necessary to account for empty fields.
    On Error Resume Next
    txtNumber.Text = CDbl(txtNumber.Text)
End Sub

In some cases, however, formatting and unformatting a value isn't that simple. For example, you can format a Currency value to add parentheses around negative numbers, but there's no built-in Visual Basic function able to return a string formatted in that way to its original condition. Fear not, because nothing prevents you from creating your own formatting and unformatting routines. I have built two general-purpose routines for you to consider.

The FilterString routine filters out all unwanted characters in a string:

Function FilterString(Text As String, validChars As String) As String
    Dim i As Long, result As String
    For i = 1 To Len(Text)
        If InStr(validChars, Mid$(Text, i, 1)) Then
            result = result & Mid$(Text, i, 1)
        End If
    Next
    FilterString = result
End Function

FilterNumber builds on FilterString to strip down all formatting characters in a number and can also trim trailing decimal zeros:

Function FilterNumber(Text As String, TrimZeros As Boolean) As String
    Dim decSep As String, i As Long, result As String
    ' Retrieve the decimal separator symbol.
    decSep = Format$(0.1, ".")
    ' Use FilterString for most of the work.
    result = FilterString(Text, decSep & "-0123456789")
    ' Do the following only if there is a decimal part and the
    ' user requested that nonsignificant digits be trimmed.
    If TrimZeros And InStr(Text, decSep) > 0 Then
        For i = Len(result) To 1 Step -1
            Select Case Mid$(result, i, 1)
                Case decSep
                    result = Left$(result, i - 1)
                    Exit For
                Case "0"
                    result = Left$(result, i - 1)
                Case Else
                    Exit For
            End Select
        Next
    End If
    FilterNumber = result
End Function

The feature I like most in FilterNumber is that it's locale-independent. It works equally well on both sides of the Atlantic ocean (and on other continents, as well.) Instead of hard-coding the decimal separator character in the code, the routine determines it on the fly, using the Visual Basic for Applications (VBA) Format function. Start thinking internationally now, and you won't have a nervous breakdown when you have to localize your applications in German, French, and Japanese.

TIP
The Format function lets you retrieve many locale-dependent characters and separators.

Format$(0.1, ".")                           ' Decimal separator
Format$(1, ",")                             ' Thousand separator
Mid$(Format(#1/1/99#, "short date"), 2, 1)  ' Date separator

You can also determine whether the system uses dates in "mm/dd/yy" (U.S.) format or "dd/mm/yy" (European) format, using this code:

If Left$(Format$("12/31/1999", "short date"), 2) = 12 Then
    ' mm/dd/yy format

Else
    ' dd/mm/yyyy format

End If

There's no direct way to determine the currency symbol, but you can derive it by analyzing the result of this function:

Format$(0, "currency")                      ' Returns "$0.00" in US

It isn't difficult to write a routine that internally uses the information I've just given you to extract the currency symbol as well as its default position (before or after the number) and the default number of decimal digits in currency values. Remember, in some countries the currency symbol is actually a string of two or more characters.

To illustrate these concepts in action, I've built a simple demonstration program that shows how you can format numbers, currency values, dates, phone numbers, and credit-card numbers when exiting a field, and how you can remove that formatting from the result when the input focus reenters the TextBox control. Figure 3-2 shows the formatted results.

Click to view at full size.

Figure 3-2. Formatting and unformatting the contents of TextBox controls makes for more professional-looking applications.

Private Sub txtNumber_GotFocus()
    ' Filter out nondigit chars and trailing zeros.
    On Error Resume Next
    txtNumber.Text = FilterNumber(txtNumber.Text, True)
End Sub
Private Sub txtNumber_LostFocus()
    ' Format as a number, grouping thousand digits.
    On Error Resume Next
    txtNumber.Text = Format(CDbl(txtNumber.Text), _
        "#,###,###,##0.######")
End Sub

Private Sub txtCurrency_GotFocus()
    ' Filter out nondigit chars and trailing zeros.
    ' Restore standard text color.
    On Error Resume Next
    txtCurrency.Text = FilterNumber(txtCurrency.Text, True)
    txtCurrency.ForeColor = vbWindowText
End Sub
Private Sub txtCurrency_LostFocus()
    On Error Resume Next
    ' Show negative values as red text.
    If CDbl(txtCurrency.Text) < 0 Then txtCurrency.ForeColor = vbRed
    ' Format currency, but don't use parentheses for negative numbers.
    ' (FormatCurrency is a new VB6 string function.)
    txtCurrency.Text = FormatCurrency(txtCurrency.Text, , , vbFalse)
End Sub

Private Sub txtDate_GotFocus()
    ' Prepare to edit in short-date format.
    On Error Resume Next
    txtDate.Text = Format$(CDate(txtDate.Text), "short date")
End Sub
Private Sub txtDate_LostFocus()
    ' Convert to long-date format upon exit.
    On Error Resume Next
    txtDate.Text = Format$(CDate(txtDate.Text), "d MMMM yyyy")
End Sub

Private Sub txtPhone_GotFocus()
    ' Trim embedded dashes.
    txtPhone.Text = FilterString(txtPhone.Text, "0123456789")
End Sub
Private Sub txtPhone_LostFocus()
    ' Add dashes if necessary.
    txtPhone.Text = FormatPhoneNumber(txtPhone.Text)
End Sub

Private Sub txtCreditCard_GotFocus()
    ' Trim embedded spaces.
    txtCreditCard.Text = FilterNumber(txtCreditCard.Text, True)
End Sub
Private Sub txtCreditCard_LostFocus()
    ' Add spaces if necessary.
    txtCreditCard.Text = FormatCreditCard(txtCreditCard.Text)
End Sub

Instead of inserting the code that formats phone numbers and credit-card numbers right in the LostFocus event procedures, I built two distinct routines, which can be more easily reused in other applications, as shown in the code below.

Function FormatPhoneNumber(Text As String) As String
    Dim tmp As String
    If Text <> "" Then
        ' First get rid of all embedded dashes, if any.
        tmp = FilterString(Text, "0123456789")
        ' Then reinsert them in the correct position.
        If Len(tmp) <= 7 Then
            FormatPhoneNumber = Format$(tmp, "!@@@-@@@@")
        Else
            FormatPhoneNumber = Format$(tmp, "!@@@-@@@-@@@@")
        End If
    End If
End Function

Function FormatCreditCard(Text As String) As String
    Dim tmp As String
    If Text <> "" Then
        ' First get rid of all embedded spaces, if any.
        tmp = FilterNumber(Text, False)
        ' Then reinsert them in the correct position.
        FormatCreditCard = Format$(tmp, "!@@@@ @@@@ @@@@ @@@@")
    End If
End Function

Unfortunately, there isn't any way to create locale-independent routines that can format any phone number anywhere in the world. But by grouping all your formatting routines in one module, you can considerably speed up your work if and when it's time to convert your code for another locale. Chapter 5 covers the Format function in greater detail.

Multiline TextBox Controls

You create multiline TextBox controls by setting the MultiLine property to True and the ScrollBars property to 2-Vertical or 3-Both. A vertical scroll bar causes the contents of the control to automatically wrap when a line is too long for the control's width, so this setting is most useful when you're creating memo fields or simple word processor-like programs. If you have both a vertical and a horizontal scroll bar, the TextBox control behaves more like a programmer's editor, and longer lines simply extend beyond the right border. I've never found a decent use for the other settings of the ScrollBars property (0-None and 1-Horizontal) in a multiline TextBox control. Visual Basic ignores the ScrollBars property if MultiLine is False.

Both these properties are read-only at run time, which means that you can't alternate between a regular and a multiline text box, or between a word processor-like multiline field (ScrollBars = 2-Vertical) and an editorlike field (ScrollBars = 3-Both). To tell the whole truth, Visual Basic's support for multiline TextBox controls leaves much to be desired. You can do very little with such controls at run time, except to retrieve and set their Text properties. When you read the contents of a multiline TextBox control, it's up to you to determine where each line of text starts and ends. You do this with a loop that searches for carriage return (CR) and line feed (LF) pairs, or even more easily using the new Split string function:

' Print the lines of text in Text1, labeling them with their line numbers.
Dim lines() As String, i As Integer
lines() = Split(Text1.Text, vbCrLf)
For i = 0 To UBound(lines)
    Print (i + 1) & ": " & lines(i)
Next

The support offered by Visual Basic for multiline TextBox controls ends here. The language doesn't offer any means for learning such vital information as at which point each line of text wraps, which are the first visible line and the first visible column, which line and column the caret is on, and so on. Moreover, you have no means of programmatically scrolling through a multiline text box. The solutions to these problems require Microsoft Windows API programming, which I'll explain in the Appendix. In my opinion, however, Visual Basic should offer these features as built-in properties and methods.

You should account for two minor issues when including one or more multiline TextBox controls on your forms. When you enter code in a word processor or an editor, you expect that the Enter key will add a newline character (more precisely, a CR-LF character pair) and that the Tab key will insert a tab character and move the caret accordingly. Visual Basic supports these keys, but because both of them have special meaning to Windows the support is limited: The Enter key adds a CR-LF pair only if there isn't a default push button on the form, and the Tab key inserts a tab character only if there aren't other controls on the form whose TabStop property is set to True. In many circumstances, these requirements can't be met, and some of your users will find your user interface annoying. If you can't avoid this problem, at least add a reminder to your users that they can add new lines using the Ctrl+Enter key combination and insert tab characters using the Ctrl+Tab key combination. Another possible approach is to set the TabStop property to False for all the controls in the form in the multiline TextBox's GotFocus event and to restore the original values in the LostFocus event procedure.